<!--
  @desc 大数据表格，虚拟滚动实时渲染
  @author 石艳军
  @date 2023-11-24
  @url https://xiaocheng555.github.io/el-table-virtual-scroll/#/CaseDemo?code=CaseDemo&codeTitle=%E6%A1%88%E4%BE%8B
  属性:
  - loading { boolean } 加载loading
  - columns { array } 表格列配置项，必传项
    {
      - type { string } 对应列的类型，可选项: selection | index | expand。如果设置了 selection 则显示多选框；如果设置了 index 则显示该行的索引（从 1 开始计算）；如果设置了 expand 则显示为一个可展开的按钮
      - title { string } 显示的标题
      - renderHeader { Function(h, { column, $index }) } 列标题 Label 区域渲染使用的 Function
      - dataIndex { string } 对应列内容的字段名,唯一值
      - width { number } 对应列的宽度
      - minWidth { number } 对应列的最小宽度，与 width 的区别是 width 是固定的，min-width 会把剩余宽度按比例分配给设置了 min-width 的列
      - fixed { string, boolean } 列是否固定在左侧或者右侧，true 表示固定在左侧; 可选值: true | left | right
      - reserveSelection { boolean } 仅对 type=selection 的列有效，类型为 Boolean，为 true 则会在数据更新之后保留之前选中的数据（需指定 row-key）,默认为: false
      - showOverflowTooltip { boolean } 当内容过长被隐藏时显示 tooltip, 默认: true
      - align { string } 对齐方式, 可选值：left/center/right, 默认: center
      - headerAlign { string } 表头对齐方式，若不设置该项，则使用表格的对齐方式, 可选值：left/center/right, 默认: center
      - scopedSlots { object } 使用 columns 时，可以通过该属性配置支持 slot-scope 的属性，如 scopedSlots: { customRender: 'XXX'}
    }
  - dataSource { Array } 数据源对象，必传项
  - stripe { boolean } 是否为斑马纹 table
  - border { boolean } 是否带有纵向边框
  - size { boolean } Table 的尺寸
  - fit { boolean } 列的宽度是否自撑开, 默认为: true
  - rowKey { Function(row)/String } 行数据的 Key，用来优化 Table 的渲染；在使用 reserve-selection 功能与显示树形数据时，该属性是必填的。
                        类型为 String 时，支持多层访问：user.info.id，但不支持 user.info[0].id，此种情况请使用 Function。

  使用方式:
    const columns = [
      { type: 'selection', reserveSelection: true, fixed: 'left' },
      { type: 'index', title: '序号', width: 60, fixed: 'left' },
      { dataIndex: 'riskUnitName', title: '单位名称', width: 200, fixed: 'left', scopedSlots: { customRender: 'riskUnitName' } },
      { dataIndex: 'economicTypeName', title: '行业经济分类' },
      { dataIndex: 'action', title: '操作', width: 100, fixed: 'right', scopedSlots: { customRender: 'action' } },
    ];

    <base-virtual-table
        ref="baseVirtualTableRef"
        :loading="loading"
        :columns="tableColumns"
        :height="500"
        :data-source="virtualData"
        :is-infinite-scroll="false"
        :infinite-scroll-disabled="noData"
        stripe
        border
        @selection-change="onSelectionChange"
        @load-more="onLoadMore"
      >
        <template #action="scope">
          <base-icon-button icon="el-icon-edit" text="编辑" @click="onEdit(scope.row)"></base-icon-button>
        </template>
      </base-virtual-table>
-->
<template>
  <virtual-scroll
    ref="virtualScrollRef"
    class="base-virtual-scroll"
    :data="dataSource"
    :key-prop="rowKey"
    :item-size="itemSize"
    :dynamic="dynamic"
    :virtualized="virtualized"
    @change="onVirtualScrollChange"
    @selection-change="onSelectionChange"
  >
    <template #default="{ headerCellFixedStyle, cellFixedStyle }">
      <el-table
        ref="tableRef"
        v-el-table-infinite-scroll="onLoadMore"
        v-loading="loading"
        :infinite-scroll-disabled="infiniteScrollDisabled || !isInfiniteScroll"
        :infinite-scroll-delay="infiniteScrollDelay"
        :infinite-scroll-distance="infiniteScrollDistance"
        :infinite-scroll-immediate="true"
        :headerCellStyle="headerCellFixedStyle"
        :cellStyle="cellFixedStyle"
        :highlight-current-row="highlightCurrentRow"
        :row-key="rowKey"
        :data="tableData"
        :height="height"
        :stripe="stripe"
        :border="border"
        :size="size"
        :fit="fit"
        :lazy="lazy"
        :load="load"
        :tree-props="treeProps"
        @row-click="(row, column, event) => $emit('row-click', row, column, event)"
        @header-dragend="onHeaderDragend"
        @expand-change="onExpandChange"
      >
        <template v-for="(column, columnIndex) in columns">
          <virtual-column
            v-if="column.scopedSlots && column.scopedSlots.customRender"
            :key="column.key || columnIndex"
            :label="column.title"
            :width="column.width"
            :min-width="column.minWidth"
            :vfixed="column.fixed"
            :show-overflow-tooltip="column.showOverflowTooltip"
            :align="column.align"
            :header-align="column.headerAlign"
            :selectable="column.selectable"
            :reserve-selection="column.reserveSelection"
          >
            <template #default="scope">
              <slot v-if="scope.row" :name="column.scopedSlots.customRender" :row="scope.row" :$index="scope.$index"></slot>
            </template>
          </virtual-column>

          <virtual-column
            v-else
            :key="column.key || columnIndex"
            :type="column.type"
            :prop="column.dataIndex"
            :label="column.title"
            :width="column.width"
            :min-width="column.minWidth"
            :vfixed="column.fixed"
            :show-overflow-tooltip="column.showOverflowTooltip"
            :align="column.align"
            :header-align="column.headerAlign"
            :selectable="column.selectable"
            :reserve-selection="column.reserveSelection"
          ></virtual-column>
        </template>

        <div v-if="loading && isInfiniteScroll" class="buttonLoading" slot="append">{{ loadingTips }}</div>
      </el-table>
    </template>
  </virtual-scroll>
</template>

<script>
import _ from 'lodash';
import { VirtualScroll, VirtualColumn } from 'el-table-virtual-scroll';
import ElTableInfiniteScroll from 'el-table-infinite-scroll';


export default {
  name: 'base-virtual-table',
  components: { VirtualScroll, VirtualColumn },
  directives: { 'el-table-infinite-scroll': ElTableInfiniteScroll },
  props: {
    // 列表数据源
    dataSource: {
      type: Array,
      required: true,
    },
    // 列配置项
    columns: {
      type: Array,
      required: true,
    },
    loading: {
      type: Boolean,
      default: false,
    },
    // 滚动加载提示语
    loadingTips: {
      type: String,
      default: '加载中...',
    },
    // 是否开启滚动加载
    isInfiniteScroll: {
      type: Boolean,
      default: false,
    },
    // 是否禁用无限虚拟滚动
    infiniteScrollDisabled: {
      type: Boolean,
      default: false,
    },
    // 节流时延，单位为: ms
    infiniteScrollDelay: {
      type: Number,
      default: 200,
    },
    // 触发加载的距离阈值，单位为: px
    infiniteScrollDistance: {
      type: Number,
      default: 0,
    },
    // 表格高度，必传
    height: {
      type: Number,
      required: true,
      default: 0,
    },
    // 是否高亮当前行
    highlightCurrentRow: {
      type: Boolean,
      default: true,
    },
    // key值，data数据中的唯一id【若 rowKey 未设置或 rowKey 值不唯一，可能导致表格空数据或者滚动时渲染的数据断层、不连贯、滚动不了】
    rowKey: {
      type: String,
      default: 'id',
    },
    // 动态获取表格行高度，默认开启。设置为false时，则以itemSize为表格行的真实高度，能大大减少虚拟滚动计算量，减少滚动白屏；
    // 如果itemSize与表格行的真实高度不一致，可能导致滚动时表格数据错乱、抖动、底部有空白
    dynamic: {
      type: Boolean,
      default: false,
    },
    stripe: {
      type: Boolean,
      default: false,
    },
    border: {
      type: Boolean,
      default: false,
    },
    size: {
      type: String,
      default: 'small',  // medium | small | mini
    },
    fit: {
      type: Boolean,
      default: true,
    },
    /**
     * 以下为树形结构配置项
     */
    // 是否懒加载子节点数据
    lazy: {
      type: Boolean,
      default: false,
    },
    // 加载子节点数据的函数，lazy 为 true 时生效，函数第二个参数包含了节点的层级信息
    // Function(row, treeNode, resolve)
    load: {
      type: Function,
    },
    // 渲染嵌套数据的配置选项 { hasChildren: 'hasChildren', children: 'children' }
    treeProps: {
      type: Object,
    },
  },
  data() {
    return {
      tableData: [],
    };
  },
  watch: {
    columns: {
      handler(list) {
        if (list) {
          list.forEach(item => {
            item.minWidth = item.minWidth || '50px';
            if (_.isUndefined(item.showOverflowTooltip)) {
              // 默认超出部分 tooltip 提示
              item.showOverflowTooltip = true;
            }
            if (['selection', 'index'].includes(item.type)) {
              item.align = 'center';
              item.headerAlign = 'center';
            }
          });
        }
      },
      immediate: true,
      deep: true,
    },
  },
  computed: {
    // 数据量超过100条时，默认开启虚拟滚动
    virtualized() {
      return Array.isArray(this.dataSource) && this.dataSource.length > 100;
    },
    itemSize() {
      const rowHeightMap = {
        'mini': 35,
        'small': 45,
        'medium': 55,
      };
      return rowHeightMap[this.size] || 55;
    },
  },

  methods: {
    /**
     * 滚动到第几行
     * @param index
     */
    scrollTo(index) {
      this.$refs.virtualScrollRef.scrollTo(index);
    },

    /**
     * 更新，会重新计算实际渲染数据和位置
     */
    update() {
      this.$refs.virtualScrollRef.update();
    },

    /**
     * 重置
     */
    reset() {
      this.$refs.virtualScrollRef.reset();
    },

    /**
     * 用于多选 <virtual-column type="selection">, 切换某一行的选中状态，如果使用了第二个参数，则是设置这一行选中与否（selected 为 true 则选中）
     * @param row
     * @param selected
     */
    toggleRowSelection(row, selected) {
      this.$refs.virtualScrollRef.toggleRowSelection(row, selected);
    },

    /**
     * 用于展开行 <virtual-column type="expand">, 切换某一行的展开状态，如果使用了第二个参数，则是设置这一行展开与否（expanded 为 true 则展开）
     * @param row
     * @param expanded
     */
    toggleRowExpansion(row, expanded) {
      this.$refs.virtualScrollRef.toggleRowExpansion(row, expanded);
    },

    /**
     * 用于多选 <virtual-column type="selection">，清空用户的选择
     */
    clearSelection() {
      this.$refs.virtualScrollRef.clearSelection();
    },

    /**
     * 设定某一行为选中行，如果调用时不加参数，则会取消目前高亮行的选中状态
     * @param row
     */
    setCurrentRow(row) {
      this.$refs['tableRef'].setCurrentRow(row);
    },

    /**
     * 计算完成真实显示的表格行数
     */
    onVirtualScrollChange(renderData, start, end) {
      this.tableData = renderData;
    },

    /**
     * 虚拟表格多选选项发生更改时触发事件
     */
    onSelectionChange(selectedRows) {
      this.$emit('selection-change', selectedRows);
    },

    /**
     * 手动拖拽更改列宽
     */
    onHeaderDragend() {
      this.$refs.virtualScrollRef.doHeaderLayout();
    },

    /**
     * 当用户对某一行展开或者关闭的时候会触发该事件（展开行时，回调的第二个参数为 expandedRows；树形表格时第二参数为 expanded）
     * 由于树节点（已展开）突然收起时，会出现的当前渲染的表格行不能满屏情况，需要更新virtualList组件，重新计算需要渲染的数据
     */
    onExpandChange(row, expanded) {
      this.$emit('expand-change', row, expanded);
      this.$refs.virtualScrollRef.update();
    },

    /**
     * 加载更多数据
     */
    onLoadMore() {
      if (!this.isInfiniteScroll || this.loading) return;
      this.$emit('load-more');
    },
  },
};
</script>

<style lang="scss" scoped>
.base-virtual-scroll {
  .el-table {
    width: 100%;

    &.el-table--mini {
      ::v-deep .el-table__cell {
        height: 25px;
      }

      ::v-deep .el-table__header-wrapper,
      ::v-deep .el-table__fixed-header-wrapper {
        thead {
          tr, th {
            height: 30px;
          }
        }
      }
    }

    &.el-table--small {
      ::v-deep .el-table__cell {
        height: 35px;
      }

      ::v-deep .el-table__header-wrapper,
      ::v-deep .el-table__fixed-header-wrapper {
        thead {
          tr, th {
            height: 40px;
          }
        }
      }
    }

    &.el-table--medium {
      ::v-deep .el-table__cell {
        height: 45px;
      }

      ::v-deep .el-table__header-wrapper,
      ::v-deep .el-table__fixed-header-wrapper {
        thead {
          tr, th {
            height: 50px;
          }
        }
      }
    }

    ::v-deep .el-table__cell {
      color: #5D6B81;
    }

    ::v-deep .el-table__header-wrapper,
    ::v-deep .el-table__fixed-header-wrapper {
      thead {
        tr, th {
          background-color: #F2F2F2;
          color: #23313e ;
          height: 50px;
        }
      }
    }

    ::v-deep .el-table__body-wrapper {
      th, td {
        &.el-table-column--selection {
          .cell {
            padding-left: 10px;
            padding-right: 10px;
          }
        }
      }
    }

    &.el-table--border {
      ::v-deep td, th {
        border-right: 1px solid #ddd;
      }
    }
  }
}

.buttonLoading {
  text-align: center;
  padding: 12px;
  color: #999999;
}
</style>

